/**
 * $RCSfile$
 * $Revision: $
 * $Date: $
 *
 * Copyright 2003-2006 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.smackx.filetransfer;

import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.XMPPError;

import java.io.*;

/**
 * Handles the sending of a file to another user. File transfer's in jabber have
 * several steps and there are several methods in this class that handle these
 * steps differently.
 *
 * @author Alexander Wenckus
 *
 */
public class OutgoingFileTransfer extends FileTransfer {

    private static int RESPONSE_TIMEOUT = 60 * 1000;
    private NegotiationProgress callback;

    /**
     * Returns the time in milliseconds after which the file transfer
     * negotiation process will timeout if the other user has not responded.
     *
     * @return Returns the time in milliseconds after which the file transfer
     *         negotiation process will timeout if the remote user has not
     *         responded.
     */
    public static int getResponseTimeout() {
        return RESPONSE_TIMEOUT;
    }

    /**
     * Sets the time in milliseconds after which the file transfer negotiation
     * process will timeout if the other user has not responded.
     *
     * @param responseTimeout
     *            The timeout time in milliseconds.
     */
    public static void setResponseTimeout(int responseTimeout) {
        RESPONSE_TIMEOUT = responseTimeout;
    }
    private OutputStream outputStream;
    private String initiator;
    private Thread transferThread;

    protected OutgoingFileTransfer(String initiator, String target,
            String streamID, FileTransferNegotiator transferNegotiator) {
        super(target, streamID, transferNegotiator);
        this.initiator = initiator;
    }

    protected void setOutputStream(OutputStream stream) {
        if (outputStream == null) {
            this.outputStream = stream;
        }
    }

    /**
     * Returns the output stream connected to the peer to transfer the file. It
     * is only available after it has been succesfully negotiated by the
     * {@link StreamNegotiator}.
     *
     * @return Returns the output stream connected to the peer to transfer the
     *         file.
     */
    protected OutputStream getOutputStream() {
        if (getStatus().equals(FileTransfer.Status.negotiated)) {
            return outputStream;
        } else {
            return null;
        }
    }

    /**
     * This method handles the negotiation of the file transfer and the stream,
     * it only returns the created stream after the negotiation has been completed.
     *
     * @param fileName
     *            The name of the file that will be transmitted. It is
     *            preferable for this name to have an extension as it will be
     *            used to determine the type of file it is.
     * @param fileSize
     *            The size in bytes of the file that will be transmitted.
     * @param description
     *            A description of the file that will be transmitted.
     * @return The OutputStream that is connected to the peer to transmit the
     *         file.
     * @throws XMPPException
     *             Thrown if an error occurs during the file transfer
     *             negotiation process.
     */
    public synchronized OutputStream sendFile(String fileName, long fileSize,
            String description) throws XMPPException {
        if (isDone() || outputStream != null) {
            throw new IllegalStateException(
                    "The negotation process has already" + " been attempted on this file transfer");
        }
        try {
            this.outputStream = negotiateStream(fileName, fileSize, description);
        } catch (XMPPException e) {
            handleXMPPException(e);
            throw e;
        }
        return outputStream;
    }

    /**
     * This methods handles the transfer and stream negotiation process. It
     * returns immediately and its progress will be updated through the
     * {@link NegotiationProgress} callback.
     *
     * @param fileName
     *            The name of the file that will be transmitted. It is
     *            preferable for this name to have an extension as it will be
     *            used to determine the type of file it is.
     * @param fileSize
     *            The size in bytes of the file that will be transmitted.
     * @param description
     *            A description of the file that will be transmitted.
     * @param progress
     *            A callback to monitor the progress of the file transfer
     *            negotiation process and to retrieve the OutputStream when it
     *            is complete.
     */
    public synchronized void sendFile(final String fileName,
            final long fileSize, final String description,
            final NegotiationProgress progress) {
        if (progress == null) {
            throw new IllegalArgumentException("Callback progress cannot be null.");
        }
        checkTransferThread();
        if (isDone() || outputStream != null) {
            throw new IllegalStateException(
                    "The negotation process has already" + " been attempted for this file transfer");
        }
        this.callback = progress;
        transferThread = new Thread(new Runnable() {

            public void run() {
                try {
                    OutgoingFileTransfer.this.outputStream = negotiateStream(
                            fileName, fileSize, description);
                    progress.outputStreamEstablished(OutgoingFileTransfer.this.outputStream);
                } catch (XMPPException e) {
                    handleXMPPException(e);
                }
            }
        }, "File Transfer Negotiation " + streamID);
        transferThread.start();
    }

    private void checkTransferThread() {
        if (transferThread != null && transferThread.isAlive() || isDone()) {
            throw new IllegalStateException(
                    "File transfer in progress or has already completed.");
        }
    }

    /**
     * This method handles the stream negotiation process and transmits the file
     * to the remote user. It returns immediatly and the progress of the file
     * transfer can be monitored through several methods:
     *
     * <UL>
     * <LI>{@link FileTransfer#getStatus()}
     * <LI>{@link FileTransfer#getProgress()}
     * <LI>{@link FileTransfer#isDone()}
     * </UL>
     *
     * @param file the file to transfer to the remote entity.
     * @param description a description for the file to transfer.
     * @throws XMPPException
     *             If there is an error during the negotiation process or the
     *             sending of the file.
     */
    public synchronized void sendFile(final File file, final String description)
            throws XMPPException {
        //System.out.println("OutgoingFileTransfer.sendFile()");
        checkTransferThread();
        if (file == null || !file.exists() || !file.canRead()) {
            throw new IllegalArgumentException("Could not read file");
        } else {
            setFileInfo(file.getAbsolutePath(), file.getName(), file.length());
        }


        transferThread = new Thread(new Runnable() {

            public void run() {
                System.out.println("OutgoingFileTransfer.transferThread.run()");
                try {
                    outputStream = negotiateStream(file.getName(), file.length(), description);
                    //System.out.println("OutgoingFileTransfer.transferThread.run() (outputStream == null)? " + (outputStream == null));
                } catch (XMPPException e) {
                    //System.out.println("OutgoingFileTransfer.transferThread.run() 1 XMPPException e");
                    handleXMPPException(e);
                    return;
                } finally {
                    //System.out.println("transferThread.run() finally");
                }
                if (outputStream == null) {
                    //System.out.println("transferThread.run() outputStream is null");
                    return;
                }

                if (!updateStatus(Status.negotiated, Status.in_progress)) {
                    //System.out.println("OutgoingFileTransfer.transferThread.run() !updateStatus");
                    return;
                }

                InputStream inputStream = null;
                try {
                    //System.out.println("OutgoingFileTransfer.transferThread.run() try");

                    inputStream = new FileInputStream(file);
                    //boolean isNull = (inputStream == null);
                    //System.out.println("OutgoingFileTransfer.transferThread.run() inputStream == null? " + isNull);
                    /*try {
                    System.out.println("OutgoingFileTransfer.transferThread.run() inputStream.available " + inputStream.available());
                    } catch (IOException ex) {
                    ex.printStackTrace();
                    }*/
                    writeToStream(inputStream, outputStream);
                } catch (FileNotFoundException e) {
                    System.out.println("OutgoingFileTransfer.transferThread.run() FileNotFoundException e");
                    setStatus(FileTransfer.Status.error);
                    setError(Error.bad_file);
                    setException(e);
                } catch (XMPPException e) {
                    System.out.println("OutgoingFileTransfer.transferThread.run() 2 XMPPException e");
                    setStatus(FileTransfer.Status.error);
                    setException(e);
                } finally {
                    try {
                        if (inputStream != null) {
                            inputStream.close();
                        }

                        outputStream.flush();
                        outputStream.close();
                    } catch (IOException e) {
                        /* Do Nothing */
                    }
                }
                updateStatus(Status.in_progress, FileTransfer.Status.complete);
                System.out.println("OutgoingFileTransfer.transferThread.run() done");

            }
        }, "File Transfer " + streamID);
        transferThread.start();
    }

    /**
     * This method handles the stream negotiation process and transmits the file
     * to the remote user. It returns immediatly and the progress of the file
     * transfer can be monitored through several methods:
     *
     * <UL>
     * <LI>{@link FileTransfer#getStatus()}
     * <LI>{@link FileTransfer#getProgress()}
     * <LI>{@link FileTransfer#isDone()}
     * </UL>
     *
     * @param file the file to transfer to the remote entity.
     * @param description a description for the file to transfer.
     * @throws XMPPException
     *             If there is an error during the negotiation process or the
     *             sending of the file.
     */
    public synchronized void sendStream(final String name, final InputStream inputStream, final String description)
            throws XMPPException {
        System.out.println("OutgoingFileTransfer.sendStream()");
        checkTransferThread();
        int available = 0;
        if (inputStream == null) {
            throw new IllegalArgumentException("Could not read from stream");
        } else {
            try {
                available = inputStream.available();
                System.out.println("OutgoingFileTransfer.sendStream() available: " + available);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            setFileInfo("/", name, available);
        }

        final int available2 = available;

        transferThread = new Thread(new Runnable() {

            public void run() {
                System.out.println("OutgoingFileTransfer.transferThread.run()");
                try {

                    outputStream = negotiateStream(name, available2, description);
                    //System.out.println("OutgoingFileTransfer.transferThread.run() (outputStream == null)? " + (outputStream == null));
                } catch (XMPPException e) {
                    //System.out.println("OutgoingFileTransfer.transferThread.run() 1 XMPPException e");
                    handleXMPPException(e);
                    return;
                } finally {
                    //System.out.println("transferThread.run() finally");
                }
                if (outputStream == null) {
                    //System.out.println("transferThread.run() outputStream is null");
                    return;
                }

                if (!updateStatus(Status.negotiated, Status.in_progress)) {
                    //System.out.println("OutgoingFileTransfer.transferThread.run() !updateStatus");
                    return;
                }

                //InputStream inputStream = null;
                try {
                    //System.out.println("OutgoingFileTransfer.transferThread.run() try");

                    //inputStream = new FileInputStream(file);
                    //boolean isNull = (inputStream == null);
                    //System.out.println("OutgoingFileTransfer.transferThread.run() inputStream == null? " + isNull);
                    /*try {
                    System.out.println("OutgoingFileTransfer.transferThread.run() inputStream.available " + inputStream.available());
                    } catch (IOException ex) {
                    ex.printStackTrace();
                    }*/
                    writeToStream(inputStream, outputStream);
                } catch (XMPPException e) {
                    System.out.println("OutgoingFileTransfer.transferThread.run() 2 XMPPException e");
                    setStatus(FileTransfer.Status.error);
                    setException(e);
                } finally {
                    try {
                        if (inputStream != null) {
                            inputStream.close();
                        }

                        outputStream.flush();
                        outputStream.close();
                    } catch (IOException e) {
                        /* Do Nothing */
                    }
                }
                updateStatus(Status.in_progress, FileTransfer.Status.complete);
                System.out.println("OutgoingFileTransfer.transferThread.run() done");

            }
        }, "File Transfer " + streamID);
        transferThread.start();
    }

    /**
     * This method handles the stream negotiation process and transmits the file
     * to the remote user. It returns immediatly and the progress of the file
     * transfer can be monitored through several methods:
     *
     * <UL>
     * <LI>{@link FileTransfer#getStatus()}
     * <LI>{@link FileTransfer#getProgress()}
     * <LI>{@link FileTransfer#isDone()}
     * </UL>
     *
     * @param in the stream to transfer to the remote entity.
     * @param fileName the name of the file that is transferred
     * @param fileSize the size of the file that is transferred
     * @param description a description for the file to transfer.
     */
    public synchronized void sendStream(final InputStream in, final String fileName, final long fileSize, final String description) {
        checkTransferThread();

        transferThread = new Thread(new Runnable() {

            public void run() {
                //Create packet filter
                try {
                    outputStream = negotiateStream(fileName, fileSize, description);
                } catch (XMPPException e) {
                    handleXMPPException(e);
                    return;
                }
                if (outputStream == null) {
                    return;
                }

                if (!updateStatus(Status.negotiated, Status.in_progress)) {
                    return;
                }
                try {
                    writeToStream(in, outputStream);
                } catch (XMPPException e) {
                    setStatus(FileTransfer.Status.error);
                    setException(e);
                } finally {
                    try {
                        if (in != null) {
                            in.close();
                        }

                        outputStream.flush();
                        outputStream.close();
                    } catch (IOException e) {
                        /* Do Nothing */
                    }
                }
                updateStatus(Status.in_progress, FileTransfer.Status.complete);
            }
        }, "File Transfer " + streamID);
        transferThread.start();
    }

    private void handleXMPPException(XMPPException e) {
        XMPPError error = e.getXMPPError();
        if (error != null) {
            int code = error.getCode();
            System.out.println("OutgoingFileTransfer.handleXMPPException() error code: " + code);
            if (code == 403) {
                setStatus(Status.refused);
                return;
            } else if (code == 400) {
                setStatus(Status.error);
                setError(Error.not_acceptable);
            } else {
                setStatus(FileTransfer.Status.error);
            }
        }

        setException(e);
    }

    /**
     * Returns the amount of bytes that have been sent for the file transfer. Or
     * -1 if the file transfer has not started.
     * <p>
     * Note: This method is only useful when the {@link #sendFile(File, String)}
     * method is called, as it is the only method that actualy transmits the
     * file.
     *
     * @return Returns the amount of bytes that have been sent for the file
     *         transfer. Or -1 if the file transfer has not started.
     */
    public long getBytesSent() {
        return amountWritten;
    }

    private OutputStream negotiateStream(String fileName, long fileSize,
            String description) throws XMPPException {
        // Negotiate the file transfer profile

        System.out.println("OutgoingFileTransfer.negotiateStream()");
        if (!updateStatus(Status.initial, Status.negotiating_transfer)) {
            throw new XMPPException("Illegal state change");
        }
        StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer(
                getPeer(), streamID, fileName, fileSize, description,
                RESPONSE_TIMEOUT);

        if (streamNegotiator == null) {
            System.out.println("OutgoingFileTransfer.negotiateStream() streamNegotiator null");
            setStatus(Status.error);
            setError(Error.no_response);
            return null;
        } else {
            System.out.println("OutgoingFileTransfer.negotiateStream() streamNegotiator type " + streamNegotiator.getClass().getName());
        }

        // Negotiate the stream
        if (!updateStatus(Status.negotiating_transfer, Status.negotiating_stream)) {
            throw new XMPPException("Illegal state change");
        }
        outputStream = streamNegotiator.createOutgoingStream(streamID,
                initiator, getPeer());

        if (!updateStatus(Status.negotiating_stream, Status.negotiated)) {
            throw new XMPPException("Illegal state change");
        }
        System.out.println("OutgoingFileTransfer.negotiateStream() returning outputStream");
        return outputStream;
    }

    public void cancel() {
        setStatus(Status.cancelled);
    }

    protected boolean updateStatus(Status oldStatus, Status newStatus) {
        boolean isUpdated = super.updateStatus(oldStatus, newStatus);
        if (callback != null && isUpdated) {
            callback.statusUpdated(oldStatus, newStatus);
        }
        return isUpdated;
    }

    protected void setStatus(Status status) {
        Status oldStatus = getStatus();
        super.setStatus(status);
        if (callback != null) {
            callback.statusUpdated(oldStatus, status);
        }
    }

    protected void setException(Exception exception) {
        super.setException(exception);
        if (callback != null) {
            callback.errorEstablishingStream(exception);
        }
    }

    /**
     * A callback class to retrive the status of an outgoing transfer
     * negotiation process.
     *
     * @author Alexander Wenckus
     *
     */
    public interface NegotiationProgress {

        /**
         * Called when the status changes
         *
         * @param oldStatus the previous status of the file transfer.
         * @param newStatus the new status of the file transfer.
         */
        void statusUpdated(Status oldStatus, Status newStatus);

        /**
         * Once the negotiation process is completed the output stream can be
         * retrieved.
         *
         * @param stream the established stream which can be used to transfer the file to the remote
         * entity
         */
        void outputStreamEstablished(OutputStream stream);

        /**
         * Called when an exception occurs during the negotiation progress.
         *
         * @param e the exception that occured.
         */
        void errorEstablishingStream(Exception e);
    }
}
